﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;

namespace Mailias.UI.Web
{
    public class SMTPServer
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        // Thread signal.
        public static ManualResetEvent allDone = new ManualResetEvent(false);

        public static void StartListening(SMTPServerSettings settings)
        {
            // Data buffer for incoming data.
            byte[] bytes = new Byte[1024];

            logger.Info("Start Listening on Address ({0}) and port ({1})", settings.IpAddress, settings.Port);

            IPAddress ipAddress = settings.IpAddress;
            int port = settings.Port;

            IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
            
            // Create a TCP/IP socket.
            Socket listener = new Socket(AddressFamily.InterNetwork,
                                         SocketType.Stream, ProtocolType.Tcp);

            // Bind the socket to the local endpoint and listen for incoming connections.
            try
            {
                listener.Bind(localEndPoint);
                listener.Listen(100);

                while (true)
                {
                    // Set the event to nonsignaled state.
                    allDone.Reset();

                    // Start an asynchronous socket to listen for connections.
                    
                    logger.Trace("Waiting for a connection...");
                    listener.BeginAccept(
                        new AsyncCallback(AcceptCallback),
                        listener);

                    // Wait until a connection is made before continuing.
                    allDone.WaitOne();
                }

            }
            catch (Exception e)
            {
                logger.Error(e.ToString);
            }

        }

        public static void StopListening (SMTPServerSettings serttings)
        {
            
        }

        public static void AcceptCallback(IAsyncResult ar)
        {
            // Signal the main thread to continue.
            allDone.Set();

            // Get the socket that handles the client request.
            Socket listener = (Socket)ar.AsyncState;
            Socket handler = listener.EndAccept(ar);

            LogTrace(handler, "New client connection: begin new conversation");

            // Create the state object.
            StateObject state = new StateObject();
            state.workSocket = handler;
            state.ServerName = "mailias.server";
            Send(handler, string.Format("220 {0} ESMTP Mailias 1.0.0; {1}\r\n", state.ServerName, DateTime.Now.ToUniversalTime()));
            state.NextStepInConversation = StateObject.ConversationSteps.HELO;

            LogTrace(handler, " Expecting HELO from client");

            handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                                 new AsyncCallback(BeginConversationCallback), state);
        }

        public static void BeginConversationCallback(IAsyncResult ar)
        {
            String content = String.Empty;

            // Retrieve the state object and the handler socket
            // from the asynchronous state object.
            StateObject state = (StateObject)ar.AsyncState;
            Socket handler = state.workSocket;

            if( handler.Connected == false)
            {
                LogWarn(handler, "The socket has been closed but we are expecting more data.");
            }
            // Read data from the client socket.
            int bytesRead = 0;
            try
            {
                bytesRead = handler.EndReceive(ar);
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode == SocketError.ConnectionReset)
                    LogWarn(handler,"SMTP client didn't say QUIT.");
                ShutdownCallback(ar);
            }

            if (bytesRead > 0)
            {
                // There  might be more data, so store the data received so far.
                state.sb.Append(Encoding.ASCII.GetString(
                    state.buffer, 0, bytesRead));

                // Check for end-of-file tag. If it is not there, read 
                // more data.
                content = state.sb.ToString();

                if (content.EndsWith("\r\n"))
                {
                    LogTrace(handler, string.Format(" Received {0} bytes from client", content.Length));
                    LogTrace(handler, string.Format(" Expecting {0}", state.NextStepInConversation));

                    switch (state.NextStepInConversation)
                    {
                        case StateObject.ConversationSteps.HELO:
                            {
                                LogTrace(handler, " Handle HELO");
                                HandleSMTPHELO(state, handler, content);
                                break;
                            }
                        case StateObject.ConversationSteps.MAILFrom:
                            {
                                LogTrace(handler, " Handle MAILFrom");
                                HandleSMTPFrom(state, handler, content);
                                break;
                            }
                        case StateObject.ConversationSteps.RCPTTo:
                            {
                                LogTrace(handler, " Handle RCPTTo");
                                HandleSMTPRcptTo(state, handler, content);
                                break;
                            }
                        case StateObject.ConversationSteps.DATA:
                            {
                                LogTrace(handler, " Handle DATA");
                                HandleSMTPData(state, handler, content);
                                break;
                            }
                        case StateObject.ConversationSteps.EoD:
                            {
                                LogTrace(handler, " Handle EoD");
                                HandleSMTPEoD(state, handler, content);
                                break;
                            }
                    }
                    if (state.NextStepInConversation != StateObject.ConversationSteps.EoD)
                        state.sb = new StringBuilder();
                }

                if (content.IndexOf("QUIT\r\n") > -1)
                {
                    LogTrace(handler, " Handle QUIT");
                    Send(handler, "221 goodbye");
                    ShutdownCallback(ar);
                }
                else
                {
                    // Not all data received. Get more.
                    handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                                         new AsyncCallback(BeginConversationCallback), state);
                }
            }
        }

        private static void HandleSMTPEoD(StateObject state, Socket handler, string content)
        {
            if (content.Length >= 5)
            {
                const string eodString = "\r\n.\r\n";
                if (content.EndsWith(eodString))
                {
                    var lines = content.Split(new string[] {"\r\n"}, StringSplitOptions.None);

                    bool isHeader = true;
                    var headers = new List<string>();
                    var message = new StringBuilder();
                    foreach (var line in lines)
                    {
                        if (isHeader && line.Length == 0)
                        {
                            isHeader = false;
                        }
                        else
                        {
                            if (isHeader)
                            {
                                headers.Add(line);
                            }
                            else
                            {
                                message.AppendLine(line);
                            }
                        }
                    }
                    state.MailMessage.Headers.AddRange(headers);
                    state.MailMessage.Body = message.ToString();
                    state.MailMessage.Body = state.MailMessage.Body.Substring(0, state.MailMessage.Body.LastIndexOf("\r\n.\r\n"));
                    state.NextStepInConversation = StateObject.ConversationSteps.QUIT;

                    var options = new ParallelOptions();
                    options.MaxDegreeOfParallelism = Environment.ProcessorCount > 3
                                                         ? Environment.ProcessorCount - 2
                                                         : 1;

                    Task.Factory.StartNew(() =>
                                          Parallel.Invoke(options,
                                                          () =>
                                                              SaveMessage(state.MailMessage),
                                                          () =>
                                                              SendMessageToSubscribedRecipient(state.MailMessage)
                                              )
                        );
                    Debug.WriteLine(DateTime.Now.ToFileTime() + " : Created save task");

                    Send(handler, "250 Message Sent\r\n");
                }
            }
        }

        private static void HandleSMTPData(StateObject state, Socket handler, string content)
        {
            if (content.Length > 4)
            {
                string command = content.Substring(0, 4).ToLower();
                if (command == "data")
                {
                    Send(handler, "354 enter mail, end with line containing only \".\"\r\n");
                    state.NextStepInConversation = StateObject.ConversationSteps.EoD;
                    state.sb = new StringBuilder();
                }
            }
            if (state.NextStepInConversation == StateObject.ConversationSteps.DATA)
            {
                Send(handler, "500 Syntax Error, command unrecognized - Expecting: RCPT to:\r\n");
            }
        }

        private static void HandleSMTPRcptTo(StateObject state, Socket handler, string content)
        {
            if (content.Length > 8)
            {
                string command = content.Substring(0, 8).ToLower();
                if (command == "rcpt to:")
                {
                    state.MailMessage.To = content.Substring(8).Trim();
                    state.NextStepInConversation = StateObject.ConversationSteps.DATA;
                    Send(handler, string.Format("250 recipient ok\r\n"));
                }
            }
            if (state.NextStepInConversation == StateObject.ConversationSteps.RCPTTo)
            {
                Send(handler, "500 Syntax Error, command unrecognized - Expecting: RCPT to:\r\n");
            }
        }

        private static void HandleSMTPFrom(StateObject state, Socket handler, string content)
        {
            if (content.Length > 10)
            {
                string command = content.Substring(0, 10).ToLower();
                if (command == "mail from:")
                {
                    state.MailMessage.From = content.Substring(10).Trim();
                    state.NextStepInConversation = StateObject.ConversationSteps.RCPTTo;
                    Send(handler, string.Format("250 2.1.0 {0}... Sender ok\r\n", state.MailMessage.From));
                }
            }
            if (state.NextStepInConversation == StateObject.ConversationSteps.MAILFrom)
            {
                Send(handler, "500 Syntax Error, command unrecognized - Expecting: MAIL From:\r\n");
            }
        }

        private static void HandleSMTPHELO(StateObject state, Socket handler, string content)
        {
            if (content.Length > 4)
            {
                string command = content.Substring(0, 4).ToLower();
                if (command == "helo")
                {
                    Send(handler, string.Format("250 {0} Hello {1}\r\n",
                                                "mailias.server",
                                                handler.RemoteEndPoint));
                    state.NextStepInConversation = StateObject.ConversationSteps.MAILFrom;
                }
            }
            if (state.NextStepInConversation == StateObject.ConversationSteps.HELO)
            {
                Send(handler, "500 Syntax Error, command unrecognized - Expecting: HELO\r\n");
            }
        }

        private static void Send(Socket handler, String data)
        {
            // Convert the string data to byte data using ASCII encoding.
            byte[] byteData = Encoding.ASCII.GetBytes(data);

            handler.Send(byteData);
        }

        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket handler = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                int bytesSent = handler.EndSend(ar);
                Debug.WriteLine("Sent {0} bytes to client.", bytesSent);

            }
            catch (Exception e)
            {
                Debug.WriteLine(e.ToString());
            }
        }

        private static void ShutdownCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                StateObject state = (StateObject)ar.AsyncState;
                Socket handler = state.workSocket;

                handler.Shutdown(SocketShutdown.Both);
                handler.Close();
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.ToString());
            }
        }

        private static void LogTrace(Socket handle, string message)
        {
            logger.Trace("{0} - {1}", handle.Handle, message);
        }

        private static void LogDebug(Socket handle, string message)
        {
            logger.Debug("{0} - {1}", handle.Handle, message);
        }

        private static void LogInfo(Socket handle, string message)
        {
            logger.Info("{0} - {1}", handle.Handle, message);
        }

        private static void LogWarn(Socket handle, string message)
        {
            logger.Warn("{0} - {1}", handle.Handle, message);
        }

        private static void LogError(Socket handle, string message)
        {
            logger.Error("{0} - {1}", handle.Handle, message);
        }
        
        private static void ConsoleWrite(Socket handle, string message)
        {
            Debug.WriteLine(string.Format("{0} - {1}", handle.Handle, message));
        }

        public static void SaveMessage(Message message)
        {
            logger.Debug("SMTP.Service.SaveMessage - BEGIN" );

            var _documentStore = MailiasDocumentStore.Instance;
            using (var session = _documentStore.OpenSession())
            {
                if (message.CreatedOn == DateTime.MinValue)
                    message.CreatedOn = DateTime.Now;

                
                //message.Body = message.Body.Replace("=\r\n", "");
                message.Body = message.Body.Replace("=0A", "\r");
                message.Body = message.Body.Replace("=0D", "\n");
                message.Body = message.Body.Replace("=3D", "=");
                
                session.Store(message);
                session.SaveChanges();
                logger.Debug("SMTP.Service.SaveMessage - Message : " + message.Id);
            }

            logger.Debug("SMTP.Service.SaveMessage - END");
        }

        public static void SendMessageToSubscribedRecipient(Message message)
        {
            logger.Debug("SMTP.Service.SendMessageToSubscribedRecipient - BEGIN");
            System.Threading.Thread.Sleep(2000);

            using (var session = MailiasDocumentStore.Instance.OpenSession())
            {
                var subscriptionsForToAddress = session.Query<Subscription>().Where(s => s.To == message.To);
                subscriptionsForToAddress.AsParallel().ForAll(subscription => logger.Info("Send email to :" + subscription.RealEmailAddress));
            }
            logger.Error("SMTP.Service.SendMessageToSubscribedRecipient - Method not implemented yet.");
            logger.Debug("SMTP.Service.SendMessageToSubscribedRecipient - END");
        }

    }
}
